﻿/* 2014/2/15 */
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using System.Xml.Linq;
using System.IO;

namespace Cosmos.Serialization
{
    /// <summary>
    /// Serializes an object to XML string.
    /// </summary>
    /// <remarks>Only public properties which can be read and written will be serialized.</remarks>
    public static class XmlSerializer
    {
        private static readonly string _loadFromXmlArgumentExceptionMessage = "Specified type does not match the XML node.";
        private static readonly string _saveToXmlArgumentExceptionMessage = "Specified type does not match the object.";

        private const string _rootElementName = "Instance";
        private const string _typeAttributeName = "type";

        /// <summary>
        /// Load data of an object from XML.
        /// </summary>
        /// <param name="type">Type of the result object</param>
        /// <param name="xml">The XML to be deserialized.</param>
        /// <returns>Deserialized object.</returns>
        /// <exception cref="ArgumentException">Type of the object or property does not match the XML.</exception>
        /// <exception cref="FormatException">Some data are not in correct format.</exception>
        public static object Deserialize(Type type, string xml)
        {
            TextReader reader = new StringReader(xml);
            XDocument document = XDocument.Load(reader);
            
            reader.Close();

            object result = LoadFromXml(type, document.Element(_rootElementName));
            return result;
        }

        /// <summary>
        /// Save data of an object to XML.
        /// </summary>
        /// <param name="type">Type of the specified object.</param>
        /// <param name="obj">The object to be serialized.</param>
        /// <returns>Serialized XML.</returns>
        /// <exception cref="ArgumentException"></exception>
        public static string Serialize(Type type, object obj)
        {
            XDocument document = new XDocument();
            document.Add(SaveToXml(_rootElementName, type, obj));

            TextWriter writer = new StringWriter();
            document.Save(writer);

            string result = writer.ToString();
            writer.Close();

            return result;
        }

        /// <summary>
        /// Load data of an object from XML node.
        /// </summary>
        /// <exception cref="ArgumentException"></exception>
        /// <exception cref="FormatException"></exception>
        public static object LoadFromXml(Type type, XElement objectElement)
        {
            if (!type.FullName.Equals(objectElement.Attribute(_typeAttributeName).Value))
                throw new ArgumentException(_loadFromXmlArgumentExceptionMessage, "type");

            object obj;
            if (typeof(String) == type)
            {
                obj = objectElement.Value;
            }
            else if (typeof(Boolean) == type)
            {
                obj = Boolean.Parse(objectElement.Value);
            }
            else if (typeof(Int32) == type)
            {
                obj = Int32.Parse(objectElement.Value);
            }
            else
            {
                ConstructorInfo constructor = type.GetConstructor(Type.EmptyTypes);
                obj = constructor.Invoke(null);

                PropertyInfo[] properties = type.GetProperties();
                XElement propertyElement;
                object propertyObject;
                foreach (PropertyInfo property in properties)
                {
                    if (property.CanWrite && property.CanRead)
                    {
                        propertyElement = objectElement.Element(property.Name);
                        if (propertyElement != null)
                        {
                            propertyObject = LoadFromXml(property.PropertyType, propertyElement);

                            property.SetValue(obj, propertyObject, null);
                        }

                    }
                }
                propertyObject = null;
            }

            return obj;
        }

        /// <summary>
        /// Save data of an object to XML node.
        /// </summary>
        /// <exception cref="ArgumentException"></exception>
        public static XElement SaveToXml(XName name, Type type, object obj)
        {
            if (obj.GetType() != type)
                throw new ArgumentException(_saveToXmlArgumentExceptionMessage, "type");

            XElement objectElement = new XElement(name);
            XAttribute typeAttribute = new XAttribute(_typeAttributeName, type.FullName);
            objectElement.Add(typeAttribute);

            if (typeof(String) == type)
            {
                objectElement.SetValue((string)obj);
            }
            else if (typeof(Boolean) == type)
            {
                objectElement.SetValue(((bool)obj).ToString());
            }
            else if ((typeof(Int32) == type))
            {
                objectElement.SetValue(((int)obj).ToString());
            }
            else
            {
                PropertyInfo[] properties = type.GetProperties();
                XElement propertyElement;
                object propertyObject;
                foreach (PropertyInfo property in properties)
                {
                    if (property.CanRead && property.CanWrite)
                    {
                        propertyObject = property.GetValue(obj, null);
                        if (propertyObject != null)
                        {
                            propertyElement = SaveToXml(property.Name, property.PropertyType,
                                propertyObject);

                            objectElement.Add(propertyElement);
                        }
                    }
                }
                propertyObject = null;
                propertyElement = null;
            }

            return objectElement;
        }
    }
}
